﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="StereoscopicFilter.cs" company="ScRePt">
//   Copyright © ScRePt 2010 - 2012
// </copyright>
// <summary>
//   The stereoscopic filter.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace MP3D.Stereoscopic
{
    #region Using Directives

    using System;
    using System.Runtime.InteropServices;
    using System.Runtime.InteropServices.ComTypes;

    using DirectShowLib;

    using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG;

    #endregion

    /// <summary>
    /// The stereoscopic filter.
    /// </summary>
    public class StereoscopicFilter
    {
        #region Constants and Fields

        /// <summary>
        /// The filter.
        /// </summary>
        private IBaseFilter filter;

        /// <summary>
        /// The graph builder.
        /// </summary>
        private IGraphBuilder graphBuilder;

        /// <summary>
        /// The horizontal parallax.
        /// </summary>
        private int horizontalParallax;

        /// <summary>
        /// The output format.
        /// </summary>
        private int outputFormat;

        /// <summary>
        /// The vertical parallax.
        /// </summary>
        private int verticalParallax;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="StereoscopicFilter"/> class.
        /// </summary>
        public StereoscopicFilter()
            : this(null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="StereoscopicFilter"/> class.
        /// </summary>
        /// <param name="graphBuilder">
        /// The graph builder.
        /// </param>
        public StereoscopicFilter(IGraphBuilder graphBuilder)
            : this(graphBuilder, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="StereoscopicFilter"/> class.
        /// </summary>
        /// <param name="graphBuilder">
        /// The graph builder.
        /// </param>
        /// <param name="manualInitialize">
        /// The manual initialize.
        /// </param>
        public StereoscopicFilter(IGraphBuilder graphBuilder, bool manualInitialize)
            : this(graphBuilder, null, manualInitialize)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="StereoscopicFilter"/> class.
        /// </summary>
        /// <param name="graphBuilder">
        /// The graph builder.
        /// </param>
        /// <param name="filter">
        /// The filter.
        /// </param>
        public StereoscopicFilter(IGraphBuilder graphBuilder, IBaseFilter filter)
            : this(graphBuilder, filter, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="StereoscopicFilter"/> class.
        /// </summary>
        /// <param name="graphBuilder">
        /// The graph builder.
        /// </param>
        /// <param name="filter">
        /// The filter.
        /// </param>
        /// <param name="manualInitialize">
        /// The manual initialize.
        /// </param>
        public StereoscopicFilter(IGraphBuilder graphBuilder, IBaseFilter filter, bool manualInitialize)
        {
            this.ManualInitialize = true;
            this.GraphBuilder = graphBuilder;
            this.Filter = filter;
            this.ManualInitialize = manualInitialize;

            if (!this.ManualInitialize)
            {
                this.Init();
            }
        }

        #endregion

        #region Delegates

        /// <summary>
        /// The filter property changed event hander.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        public delegate void FilterPropertyChangedEventHandler(object sender, FilterPropertyChangedEventArgs e);

        #endregion

        #region Events

        /// <summary>
        /// The horizontal parallax changed.
        /// </summary>
        public event FilterPropertyChangedEventHandler HorizontalParallaxChanged;

        /// <summary>
        /// The output format changed.
        /// </summary>
        public event FilterPropertyChangedEventHandler OutputFormatChanged;

        /// <summary>
        /// The vertical parallax changed.
        /// </summary>
        public event FilterPropertyChangedEventHandler VerticalParallaxChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets DXVA.
        /// </summary>
        public bool Dxva
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return false;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                int perfFlags;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_PERFFLAGS, out perfFlags);
                bool dxva = (perfFlags & TSTFConfig.PERFFLAGS_USEDXVA2) == TSTFConfig.PERFFLAGS_USEDXVA2;
                this.Processing = false;
                return dxva;
            }

            set
            {
                if (!this.Initialized || this.Processing)
                {
                    return;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                int perfFlags;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_PERFFLAGS, out perfFlags);
                perfFlags = value ? perfFlags | TSTFConfig.PERFFLAGS_USEDXVA2 : perfFlags ^ TSTFConfig.PERFFLAGS_USEDXVA2;
                stereoscopicConfiguration.SetSTFProperty(TSTFConfig.PROP_PERFFLAGS, perfFlags);
                this.Processing = false;
            }
        }

        /// <summary>
        /// Gets or sets Deinterlace.
        /// </summary>
        public bool Deinterlace
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return false;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                int flags1;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_FLAGS1, out flags1);
                bool deinterlace = (flags1 & TSTFConfig.FLAGS_DEINTERLACE) == TSTFConfig.FLAGS_DEINTERLACE;
                this.Processing = false;
                return deinterlace;
            }

            set
            {
                if (!this.Initialized || this.Processing)
                {
                    return;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                int flags1;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_FLAGS1, out flags1);
                flags1 = value ? flags1 | TSTFConfig.FLAGS_DEINTERLACE : flags1 ^ TSTFConfig.FLAGS_DEINTERLACE;
                stereoscopicConfiguration.SetSTFProperty(TSTFConfig.PROP_FLAGS1, flags1);
                this.Processing = false;
            }
        }

        /// <summary>
        /// Gets or sets Filter.
        /// </summary>
        public IBaseFilter Filter
        {
            get
            {
                return this.filter;
            }

            set
            {
                this.filter = value;

                if (!this.ManualInitialize)
                {
                    this.Init();
                }
            }
        }

        /// <summary>
        /// Gets or sets GraphBuilder.
        /// </summary>
        public IGraphBuilder GraphBuilder
        {
            get
            {
                return this.graphBuilder;
            }

            set
            {
                this.graphBuilder = value;

                if (!this.ManualInitialize)
                {
                    this.Init();
                }
            }
        }

        /// <summary>
        /// Gets or sets HorizontalParallax.
        /// </summary>
        public int HorizontalParallax
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return 0;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_ADJUSTHORIZONTALLY, out this.horizontalParallax);
                this.Processing = false;
                return this.horizontalParallax;
            }

            set
            {
                if (!this.Initialized || this.Processing)
                {
                    return;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.SetSTFProperty(TSTFConfig.PROP_ADJUSTHORIZONTALLY, value);
                this.Processing = false;
                this.OnHorizontalParallaxChanged(this.horizontalParallax, value);
                this.horizontalParallax = value;
            }
        }

        /// <summary>
        /// Gets a value indicating whether Initialized.
        /// </summary>
        public bool Initialized { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether ManualInitialize.
        /// </summary>
        public bool ManualInitialize { get; set; }

        /// <summary>
        /// Gets or sets OutputFormat.
        /// </summary>
        public int OutputFormat
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return 0;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_METHOD, out this.outputFormat);
                this.Processing = false;
                return this.outputFormat;
            }

            set
            {
                if (!this.Initialized || this.Processing)
                {
                    return;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.SetSTFProperty(TSTFConfig.PROP_METHOD, value);
                this.Processing = false;
                this.OnOutputFormatChanged(this.outputFormat, value);
                this.outputFormat = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether Processing.
        /// </summary>
        public bool Processing { get; set; }

        /// <summary>
        /// Gets or sets SourceFormat.
        /// </summary>
        public int SourceFormat
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return 0;
                }

                this.Processing = true;
                int value;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_LAYOUT, out value);
                this.Processing = false;
                return value;
            }

            set
            {
                if (!this.Initialized || this.Processing)
                {
                    return;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.SetSTFProperty(TSTFConfig.PROP_LAYOUT, value);
                this.Processing = false;
            }
        }

        /// <summary>
        /// Gets whether DXVA is supported.
        /// </summary>
        public bool SupportsDxva
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return false;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                int perfFlags;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_PERFFLAGS, out perfFlags);
                bool dxva = (perfFlags & TSTFConfig.PERFFLAGS_DXVA2SUPPORTED) == TSTFConfig.PERFFLAGS_DXVA2SUPPORTED;
                this.Processing = false;
                return dxva;
            }
        }

        /// <summary>
        /// Gets or sets VerticalParallax.
        /// </summary>
        public int VerticalParallax
        {
            get
            {
                if (!this.Initialized || this.Processing)
                {
                    return 0;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.GetSTFProperty(TSTFConfig.PROP_ADJUSTVERTICALLY, out this.verticalParallax);
                this.Processing = false;
                return this.verticalParallax;
            }

            set
            {
                if (!this.Initialized || this.Processing)
                {
                    return;
                }

                this.Processing = true;
                ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
                stereoscopicConfiguration.SetSTFProperty(TSTFConfig.PROP_ADJUSTVERTICALLY, value);
                this.Processing = false;
                this.OnVerticalParallaxChanged(this.horizontalParallax, value);
                this.verticalParallax = value;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// The decrease horizontal parallax.
        /// </summary>
        /// <returns>
        /// The decrease horizontal parallax.
        /// </returns>
        public bool DecreaseHorizontalParallax()
        {
            if (!this.Initialized)
            {
                return false;
            }

            this.HorizontalParallax -= 16;
            return true;
        }

        /// <summary>
        /// The decrease vertical parallax.
        /// </summary>
        /// <returns>
        /// The decrease vertical parallax.
        /// </returns>
        public bool DecreaseVerticalParallax()
        {
            if (!this.Initialized)
            {
                return false;
            }

            this.VerticalParallax -= 16;
            return true;
        }

        /// <summary>
        /// The get property.
        /// </summary>
        /// <param name="propertyId">
        /// The property id.
        /// </param>
        /// <returns>
        /// The get property.
        /// </returns>
        public int GetProperty(int propertyId)
        {
            if (!this.Initialized || this.Processing)
            {
                return 0;
            }

            this.Processing = true;
            int propertyValue;
            ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
            stereoscopicConfiguration.GetSTFProperty(propertyId, out propertyValue);
            this.Processing = false;
            return propertyValue;
        }

        /// <summary>
        /// The increase horizontal parallax.
        /// </summary>
        /// <returns>
        /// The increase horizontal parallax.
        /// </returns>
        public bool IncreaseHorizontalParallax()
        {
            if (!this.Initialized)
            {
                return false;
            }

            this.HorizontalParallax += 16;
            return true;
        }

        /// <summary>
        /// The increase vertical parallax.
        /// </summary>
        /// <returns>
        /// The increase vertical parallax.
        /// </returns>
        public bool IncreaseVerticalParallax()
        {
            if (!this.Initialized)
            {
                return false;
            }

            this.VerticalParallax += 16;
            return true;
        }

        /// <summary>
        /// The init.
        /// </summary>
        /// <returns>
        /// The init.
        /// </returns>
        public bool Init()
        {
            this.Initialized = false;

            if (this.graphBuilder != null)
            {
                if (this.Filter == null)
                {
                    this.graphBuilder.FindFilterByName("3dtv.at Stereo Transformation", out this.filter);
                }

                this.Initialized = this.Filter != null;
            }

            return this.Initialized;
        }

        /// <summary>
        /// Loads a DirectShow filter with a buffer containing its parameters
        /// </summary>
        /// <param name="config">
        /// The config.
        /// </param>
        /// <returns>
        /// operation succeeded
        /// </returns>
        public bool LoadFilterConfig(byte[] config)
        {
            if (config == null)
            {
                throw new ArgumentException("the buffer is not initialised");
            }

            bool error = false;
            IStream inputstream = null;
            try
            {
                int hr = CreateStreamOnHGlobal(IntPtr.Zero, true, out inputstream);
                Marshal.ThrowExceptionForHR(hr);

                inputstream.Write(config, config.Length, IntPtr.Zero);
                inputstream.Seek(0, 0, IntPtr.Zero);
                hr = ((IPersistStream)this.filter).Load(inputstream);
                Marshal.ThrowExceptionForHR(hr);
            }
            catch
            {
                error = true;
            }
            finally
            {
                if (inputstream != null)
                {
                    System.Runtime.InteropServices.Marshal.ReleaseComObject(inputstream);
                }
            }

            return !error;
        }

        /// <summary>
        /// The save filter config.
        /// </summary>
        public byte[] SaveFilterConfig()
        {
            byte[] config;

            if (this.filter as DirectShowLib.IPersistStream == null)
            {
                throw new ArgumentException("The supplied filter does not implement IPersistStream");
            }

            IStream stream = null;
            try
            {
                // create a new stream object in memory
                int hr = CreateStreamOnHGlobal(IntPtr.Zero, true, out stream);
                Marshal.ThrowExceptionForHR(hr);

                STATSTG stats;

                // save the filter's buffer into the stream
                hr = (this.filter as DirectShowLib.IPersistStream).Save(stream, true);
                System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(hr);

                // read the stream statistics
                stream.Stat(out stats, 0);

                // reset the seek pointer to the start
                stream.Seek(0, 0, IntPtr.Zero);

                // create the buffer the same size as the stream's buffer
                config = new byte[(int)stats.cbSize];

                // read one into the other
                stream.Read(config, (int)stats.cbSize, IntPtr.Zero);
            }
            catch
            {
                config = null;
            }
            finally
            {
                if (stream != null)
                {
                    System.Runtime.InteropServices.Marshal.ReleaseComObject(stream);
                }
            }

            return config;
        }

        /// <summary>
        /// The set property.
        /// </summary>
        /// <param name="propertyId">
        /// The property id.
        /// </param>
        /// <param name="propertyValue">
        /// The property value.
        /// </param>
        /// <returns>
        /// The set property.
        /// </returns>
        public bool SetProperty(int propertyId, int propertyValue)
        {
            if (!this.Initialized || this.Processing)
            {
                return false;
            }

            if (propertyId == TSTFConfig.PROP_METHOD)
            {
                this.OutputFormat = propertyValue;
                return true;
            }

            if (propertyId == TSTFConfig.PROP_ADJUSTHORIZONTALLY)
            {
                this.HorizontalParallax = propertyValue;
                return true;
            }

            if (propertyId == TSTFConfig.PROP_ADJUSTVERTICALLY)
            {
                this.VerticalParallax = propertyValue;
                return true;
            }

            this.Processing = true;
            ISTFConfig stereoscopicConfiguration = (ISTFConfig)this.filter;
            stereoscopicConfiguration.SetSTFProperty(propertyId, propertyValue);
            this.Processing = false;
            return true;
        }

        #endregion

        #region Methods

        /// <summary>
        /// The on horizontal parallax changed.
        /// </summary>
        /// <param name="oldValue">
        /// The old value.
        /// </param>
        /// <param name="newValue">
        /// The new value.
        /// </param>
        protected void OnHorizontalParallaxChanged(int oldValue, int newValue)
        {
            if (this.HorizontalParallaxChanged != null)
            {
                this.HorizontalParallaxChanged(this, new FilterPropertyChangedEventArgs { OldValue = oldValue, NewValue = newValue });
            }
        }

        /// <summary>
        /// The on output format changed.
        /// </summary>
        /// <param name="oldValue">
        /// The old Value.
        /// </param>
        /// <param name="newValue">
        /// The new Value.
        /// </param>
        protected void OnOutputFormatChanged(int oldValue, int newValue)
        {
            if (this.OutputFormatChanged != null)
            {
                this.OutputFormatChanged(this, new FilterPropertyChangedEventArgs { OldValue = oldValue, NewValue = newValue });
            }
        }

        /// <summary>
        /// The on vertical parallax changed.
        /// </summary>
        /// <param name="oldValue">
        /// The old value.
        /// </param>
        /// <param name="newValue">
        /// The new value.
        /// </param>
        protected void OnVerticalParallaxChanged(int oldValue, int newValue)
        {
            if (this.VerticalParallaxChanged != null)
            {
                this.VerticalParallaxChanged(this, new FilterPropertyChangedEventArgs { OldValue = oldValue, NewValue = newValue });
            }
        }

        /// <summary>
        /// The create stream on h global.
        /// </summary>
        /// <param name="hglobal">
        /// The h global.
        /// </param>
        /// <param name="fdeleteOnRelease">
        /// The f delete on release.
        /// </param>
        /// <param name="ppstm">
        /// The ppstm.
        /// </param>
        /// <returns>
        /// The create stream on h global.
        /// </returns>
        [DllImport("ole32.dll")]
        private static extern int CreateStreamOnHGlobal(IntPtr hglobal, bool fdeleteOnRelease, out IStream ppstm);

        #endregion
    }

    /// <summary>
    /// The filter property changed event args.
    /// </summary>
    public class FilterPropertyChangedEventArgs : EventArgs
    {
        #region Properties

        /// <summary>
        /// Gets or sets NewValue.
        /// </summary>
        public int NewValue { get; set; }

        /// <summary>
        /// Gets or sets OldValue.
        /// </summary>
        public int OldValue { get; set; }

        #endregion
    }
}